NutriScan
Photograph a nutrition label and it becomes tracked macros: AI vision reads the calories, protein, carbs, fat and fibre straight off the packet. No barcode database, no manual typing. Around that sits a full tracker: daily goals, a food library, one-tap meal templates, weekly trends, and an AI assistant that already knows your log. It also has a public API and a Claude connector, so a claude.ai chat can read your day and log a meal by description, with every change previewed and confirmed before it saves (connector in private testing).
The scan pipeline
Images are prepared in the browser before they ever leave the phone: a touch-friendly canvas cropper isolates the label, then the image is resized and grayscaled for smaller uploads, faster scans, and cheaper AI calls. On the server every upload passes one validating gate: oversized requests are refused before the body is read, the real format is read from the file's bytes (JPEG, PNG, WebP or iPhone HEIC; the browser's label is ignored), dimensions are checked against a 30-megapixel cap before a pixel is decoded, and the image is re-encoded as a fresh JPEG with no metadata. Nothing else reaches the model, and the photo itself is never stored: only the extracted numbers are. Gemini 2.5 Flash reads the label, with automatic fallback to 3.6 Flash if the primary model fails, and its answer is checked against an explicit schema (allowed fields, sane numeric bounds; text printed on the photo is data, never instructions) and normalized into clean macros (kcal · protein · carbs · fat · fibre) before anything is shown or logged.
Auth without passwords
- ✗No passwords, ever: sign in with Google OAuth or a 6-digit emailed code. Nothing to store, leak, or reset
- ✓Verified, not trusted: every JWT decode goes through one verifying helper (shared-secret HS256 and cached JWKS) - pinned-ruleset semgrep reports zero findings, and the CI that guards it pins every action and scanner to an immutable hash
- ✓Isolated at the storage layer: each request binds the user to the database session, and row-level security policies on 17 tables do the rest, through a runtime role that cannot bypass them. App bugs can't read another user's rows
- ✓Contained when hijacked: a delete spree freezes the account on two layers (in-process lock, Supabase admin ban), revokes every API token, and every deleted row is already in a 30-day recycle bin
- ✓Deletable, with a way back: deleting starts a 15-day grace period (gated on a sign-in within the last five minutes, the JWT
amrclaim): the account turns view-and-export only, other devices are signed out, "Keep my account" undoes it, and day 15 erases every row and the login - ✓Your data, any time: export everything as an Excel workbook or a zip of CSVs, even during the grace period
F-01Row-level security everywhere
17 of the 19 tables carry forced per-user RLS policies that match nothing when the user is unset, so isolation is enforced by Postgres itself, not by remembering a WHERE clause in every query. Cross-user jobs go through narrow SECURITY DEFINER functions.
F-02Vision with a fallback
Gemini 2.5 Flash is the primary label reader; if it errors, the scan retries on 3.6 Flash automatically, and the app waits out the server's full retry budget instead of re-sending. Users see macros, not model outages.
F-03Context-aware AI assistant
A Groq-powered chat tab that's fed today's log, remaining macros and 7-day averages. Multi-turn, with server-side safety guardrails.
F-04Meal templates
Multi-select foods from the library into a named template, then log an entire breakfast in one tap instead of four.
F-05Real push notifications
VAPID web push with per-user preferences: goal alerts, meal reminders and a weekly summary, each at a time the user picks.
F-06User-controlled PWA updates
New versions don't force-reload mid-log: a banner shows exactly what changed (from a versioned changelog) and the user applies it when ready.
F-07Trends without a chart library
The 7-day trends tab renders its charts as raw SVG: no charting dependency, full control over the visuals, smaller bundle.
F-08Cost-capped by design
A 10-scans-per-day limit per account, plus per-user and per-IP burst caps checked before the paid call, keeps the vision API bill bounded while staying generous for real daily use.
F-09Abuse guards that block, not just alert
One middleware: 300+ requests a minute or a 30-URL 404 walk from one IP earns a 10-minute 429, keyed on the Cloudflare-verified client IP; 20 bad API tokens in five minutes earns the same.
F-10Recycle bin inside Postgres
SECURITY DEFINER BEFORE DELETE triggers on eleven tables copy every deleted row as JSONB into a table the app role cannot read or purge. Retention is hard-coded at 30 days; only the day-15 account purge bypasses it, and then empties that account's rows from the bin too.
F-11Two-layer account freeze
60 verified deletes in ten minutes from one session: every route returns 423 instantly from an in-process lock, and a Supabase admin ban (issued off the event loop) makes it stick across restarts. Reversal is a deliberate runbook step.
F-12Two-channel observability
Web Push to the owner's own devices for 37 hand-written ops events (quota, spikes, budget, keepalive, blocks, freezes) with per-event cooldowns and rolling-window spike detection; Sentry for unhandled exceptions with stack trace and request context.
F-13Uploads that cannot lie
One server-side gate for every image: magic-byte format sniffing (the client's content-type is never trusted), polyglot rejection, a decompression-bomb cap, verify-then-decode, and a metadata-free re-encode. An HTML file named .jpg, a PNG declaring a 30000x30000 canvas, and a photo with "ignore the label" printed on it each have a test; the model's JSON is schema-checked and rejected, never clamped.
F-14A Claude connector that asks first
A Model Context Protocol server on Supabase's OAuth 2.1 server lets claude.ai read the day and log meals. Every write tool saves nothing and returns a preview plus a one-time code; only confirming the code commits, and the code doubles as the idempotency key so a retry can never save twice.
F-15A public REST API done properly
17 endpoints with hashed, scoped personal tokens, per-user rate limits with RateLimit headers, strict request models, idempotency keys, ETag/If-Match on edits, RFC 9457 errors, previews that run the real write and roll back, and an OpenAPI file a test keeps honest.
F-16One Library, one rule
The app and Claude share one food Library. "Ask before saving new foods" is on by default and enforced by the server; it is the only setting Claude may change, and only to off.
F-17Meals with names
Everything Claude logs lands as one named meal marked "via Claude", and any meal can be opened and renamed. The grouping rides inside each row's JSON, so it needed no schema change.
F-18A budget that protects the app
The backend estimates its own free database compute use, alerts at 70% and pauses only the public API at 90%, so the app keeps working for its users. An owner-only admin panel shows it without waking the database.
F-19kcal in storage, your unit on screen
Every write is settled to kcal by one helper (a previewed migration checked all 375 stored values after a backup branch), and each account picks kcal or kJ for display, including in the chat's answers.
Scan to open NutriScan
Point your phone camera at the code to launch the live app instantly: no install needed, it runs as a PWA you can add to your home screen. Every account gets 10 free scans a day.
nutritional-tracker-delta.vercel.app github.com/MrTig-afk/NutritionalTracker